<?php

/**
 * This file is part of the Framework03 PHP framework.
 * @author Walter Tamboer
 * @package Framework03
 */


class FW_Http_Adapter_Socket extends FW_Http_Adapter_Abstract
{
    /**
     * @var resource The socket resource handle.
     */
    private $socket;
    
    /**
     * @var string The host we're currently connected to.
     */
    private $host;
    
    /**
     * @var int The port number we're currently connected to.
     */
    private $port;
    
    /**
     * @var string The current method of the request.
     */
    private $method;

    /**
     * @var array The options of this adapter.
     */
    private $options = array(
        'sslcert' => null,
        'sslpassphrase' => null,
        'ssltransport' => 'tcp',
        'timeout' => 10,
        'keepalive' => false
    );

    /**
     * Cleans up all the resources used by this class.
     */
    public function __destruct()
    {
        $this->disconnect();
    }
    
    /**
     * Sets an option of this adapter.
     * @param string $name The name of the option.
     * @param string $value The value of the option.
     * @return FW_Http_Adapter_Abstract
     */
    public function setOption($name, $value)
    {
        $this->options[$name] = $value;
        return $this;
    }

    /**
     * Connects to the given host on the given port number.
     * @param string $host The host to connect to.
     * @param int $port The port number to connect to.
     * @param bool $secure Whether or not we're connecting via a secure connection.
     */
    public function connect($host, $port, $secure)
    {
        // If the URI should be accessed via SSL, prepend the Hostname with ssl://
        $host = ($secure ? $this->options['ssltransport'] : 'tcp') . '://' . $host;

        // If we are connected to the wrong host, disconnect first
        if ((($this->host != $host) || ($this->port != $port)) && is_resource($this->socket))
            $this->disconnect();

        if (!is_resource($this->socket) || !$this->options['keepalive'])
        {
            $context = stream_context_create();

            if ($secure)
            {
                if ($this->options['sslcert'] !== null)
                {
                    if (!stream_context_set_option($context, 'ssl', 'local_cert', $this->options['sslcert']))
                        throw new FW_Http_Adapter_Exception('Unable to set sslcert option');
                }

                if ($this->options['sslpassphrase'] !== null)
                {
                    if (!stream_context_set_option($context, 'ssl', 'passphrase', $this->options['sslpassphrase']))
                        throw new FW_Http_Adapter_Exception('Unable to set sslpassphrase option');
                }
            }

            $this->socket = @stream_socket_client($host . ':' . $port,
                $errno, $errstr, (int)$this->options['timeout'], STREAM_CLIENT_CONNECT, $context);

            if (!$this->socket)
            {
                $this->disconnect();
                throw new FW_Http_Adapter_Exception('Unable to connect to ' . $host . ':' . $port . '. Error #' . $errno . ': ' . $errstr);
            }

            if (!stream_set_timeout($this->socket, (int)$this->options['timeout']))
                throw new FW_Http_Adapter_Exception('Unable to set the connection timeout');

            $this->host = $host;
            $this->port = $port;
        }
    }

    /**
     * Closes the current connection.
     */
    public function disconnect()
    {
        if (is_resource($this->socket))
        {
            @fclose($this->socket);
        }

        $this->socket = null;
    }

    /**
     * Sends a request to the remote server.
     * @param string $method The method to use.
     * @param FW_Uri_Abstract $uri The URI of this request.
     * @param string $version The HTTP version to use.
     * @param array $headers The HTTP headers to send.
     * @param string $body The body of the request.
     */
    public function write($method, FW_Uri_Abstract $uri, $version = FW_Http_Request::HTTP_1_1, $headers = array(), $body = '')
    {
        if (!$this->socket)
            throw new FW_Http_Client_Adapter_Exception('Trying to write but we are not connected');
            
        $host = $uri->getHost();
        $port = $uri->getPort();
        $host = (strtolower($uri->getScheme()) == 'https' ? $this->options['ssltransport'] : 'tcp') . '://' . $host;

        if ($this->host != $host || $this->port != $port)
            throw new FW_Http_Adapter_Exception('Trying to write but we are connected to the wrong host');
            
        $this->method = $method;

        $path = $uri->getPath();
        if ($uri->getQuery()) $path .= '?' . $uri->getQuery();

        $request = $method . ' ' . $path . ' HTTP/'. $version . "\r\n";

        foreach ($headers as $name => $value)
        {
            if (is_string($name))
                $header = ucfirst($name) . ": " . $value;

            $request .= $header . "\r\n";
        }

        $request .= "\r\n" . $body;
        
        if (!@fwrite($this->socket, $request))
            throw new FW_Http_Adapter_Exception('Error writing request to server');

        return $request;
    }

    /**
     * Reads a response from the server.
     * @return string
     */
    public function read()
    {
        $response = '';
        $gotStatus = false;
        
        while ($line = @fgets($this->socket))
        {
            $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false);

            if ($gotStatus)
            {
                $response .= $line;

                if (!chop($line)) break;
            }
        }

        // Handle 100 and 101 responses internally by restarting the read again
        if (FW_Http_Response::extractCode($response) == 100 || FW_Http_Response::extractCode($response) == 101)
            return $this->read();

        // If this was a HEAD request, return after reading the header (no need to read body)
        if ($this->method == FW_Http_Request::HEAD)
            return $response;

        // Check headers to see what kind of connection / transfer encoding we have.
        $headers = FW_Http_Response::extractHeaders($response);

        // if the connection is set to close, just read until socket closes
        if (isset($headers['connection']) && $headers['connection'] == 'close')
        {
            while ($buff = @fread($this->socket, 8192))
                $response .= $buff;

            $this->disconnect();
        }
        // Else, if we got a transfer-encoding header (chunked body)
        else if (isset($headers['transfer-encoding']))
        {
            if ($headers['transfer-encoding'] == 'chunked')
            {
                do
                {
                    $chunk = '';
                    $line = @fgets($this->socket);
                    $chunk .= $line;
                    
                    $hexchunksize = ltrim(chop($line), '0');
                    $hexchunksize = strlen($hexchunksize) ? strtolower($hexchunksize) : 0;
                    
                    $chunksize = hexdec(chop($line));
                    if (dechex($chunksize) != $hexchunksize)
                    {
                        @fclose($this->socket);
                        throw new FW_Http_Adapter_Exception('Invalid chunk size "' . $hexchunksize . '" unable to read chunked body');
                    }
                    
                    $left_to_read = $chunksize;
                    while ($left_to_read > 0)
                    {
                        $line = @fread($this->socket, $left_to_read);
                        $chunk .= $line;
                        $left_to_read -= strlen($line);
                    }
                    
                    $chunk .= @fgets($this->socket);
                    $response .= $chunk;
                }
                while ($chunksize > 0);
            }
            else
                throw new FW_Http_Adapter_Exception('Cannot handle "' . $headers['transfer-encoding'] . '" transfer encoding');
        }
        // Else, if we got the content-length header, read this number of bytes
        else if (isset($headers['content-length']))
        {
            $left_to_read = $headers['content-length'];
            $chunk = '';
            while ($left_to_read > 0)
            {
                $chunk = @fread($this->socket, $left_to_read);
                $left_to_read -= strlen($chunk);
                $response .= $chunk;
            }
        }
        // Fallback: just read the response (should not happen)
        else
        {
            while ($buff = @fread($this->socket, 8192))
                $response .= $buff;

            $this->disconnect();
        }

        return $response;
    }
}
